/*
 *Copyright 2014 DDPush
 *Author: AndyKwok(in English) GuoZhengzhu(in Chinese)
 *Email: ddpush@126.com
 *

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

*/
package org.ddpush.im.v1.client.appuser;

import java.net.SocketAddress;
import java.nio.ByteBuffer;

/**
 * 协议格式:[1字节版本号][1字节appid][1字节命令码][16字节UUID][2字节包内容长度][包内容]
 * 
 * 
 * <table width="100%" border="1" cellpadding="2" cellspacing="0">
 * <tbody>
 * <tr>
 * <td nowrap="">顺序</td>
 * <td nowrap="">名称</td>
 * <td nowrap="">代号</td>
 * <td nowrap="">格式</td>
 * <td nowrap="">字节</td>
 * <td>描述</td>
 * </tr>
 * <tr>
 * <td nowrap="">1</td>
 * <td nowrap="">版本号</td>
 * <td nowrap="">version</td>
 * <td nowrap="">无符号整数</td>
 * <td nowrap="">1</td>
 * <td>当前版本号1</td>
 * </tr>
 * <tr>
 * <td nowrap="">2</td>
 * <td nowrap="">应用ID</td>
 * <td nowrap="">appid</td>
 * <td nowrap="">无符号整数</td>
 * <td nowrap="">1</td>
 * <td>可支持255个应用同时使用一个DDPush服务器，也可以一个应用有255个分类或渠道。注：0保留，用于实时推送</td>
 * </tr>
 * <tr>
 * <td nowrap="">3</td>
 * <td nowrap="">命令</td>
 * <td nowrap="">cmd</td>
 * <td nowrap="">无符号整数</td>
 * <td nowrap="">1</td>
 * <td>最多可支持255种交互命令，目前只使用了4种</td>
 * </tr>
 * <tr>
 * <td nowrap="">4</td>
 * <td nowrap="">UUID</td>
 * <td nowrap="">uuid</td>
 * <td nowrap="">无符号整数</td>
 * <td nowrap="">16</td>
 * <td>128位uuid（注意：服务器下发的指令将不包括此项！）</td>
 * </tr>
 * <tr>
 * <td nowrap="">5</td>
 * <td nowrap="">内容长度</td>
 * <td nowrap="">length</td>
 * <td nowrap="">无符号整数</td>
 * <td nowrap="">2</td>
 * <td>支持最大65535字节的内容包长度。但udp方式下过大的包长度丢包的机率非常大，DDPush最大内容长度将不会超过500字节，一般为几十字节，
 * 提高速度和成功率</td>
 * </tr>
 * <tr>
 * <td nowrap="">6</td>
 * <td nowrap="">包内容</td>
 * <td nowrap="">content</td>
 * <td nowrap="">字节数组</td>
 * <td nowrap="">1-500</td>
 * <td>与包长度字段数值一致，最少1字节，最大不超过500字节。若包内容长度为0，则不出现此项。</td>
 * </tr>
 * </tbody>
 * </table>
 * 
 */
public final class Message {
	/** 消息版本号 */
	public static int version = 1;
	/** 服务器消息最小长度 */
	public static final int SERVER_MESSAGE_MIN_LENGTH = 5;
	/** 客户端消息最小长度 */
	public static final int CLIENT_MESSAGE_MIN_LENGTH = 21;

	// 消息类型
	/**
	 * 心跳包
	 */
	public static final int CMD_0x00 = 0;//
	/**
	 * 通用信息
	 */
	public static final int CMD_0x10 = 16;//
	/**
	 * 分类信息
	 */
	public static final int CMD_0x11 = 17;//
	/**
	 * 自定义信息
	 */
	public static final int CMD_0x20 = 32;//

	// IP套接字地址
	protected SocketAddress address;
	// 消息数据
	protected byte[] data;

	/**
	 * 使用一个 IP套接字地址SocketAddress 和一个 消息数据byte[] 来创建一个 Message对象
	 * 
	 * @param address
	 * @param data
	 */
	public Message(SocketAddress address, byte[] data) {
		this.address = address;
		this.data = data;
	}

	/**
	 * 取出data 的 SERVER_MESSAGE_MIN_LENGTH - 2 到之后的2个byte 转换成ByteBuffer
	 * 然后获取这个新ByteBuffer的值 也就是说 Message的 data 中的 SERVER_MESSAGE_MIN_LENGTH - 2
	 * 这个位置到之后的2位 用来保存这个Message的Data的数据长度 就是
	 * data的前5(SERVER_MESSAGE_MIN_LENGTH)个元素的最后两个元素 保存的是这个Message的数据长度
	 */
	public int getContentLength() {
		return (int) ByteBuffer.wrap(data, SERVER_MESSAGE_MIN_LENGTH - 2, 2).getChar();
	}

	/**
	 * 取出data的第三个元素 转成 int 并返回 看样子是这里存储的是 Message 的类型 也就是 data 的 第三个元素
	 * 保存的时这个Message的类型
	 */
	public int getCmd() {
		byte b = data[2];
		// byte 转成 int byte只有8位 直接转成int时
		// int是32位 不够的java会自动补位。
		// 所以需要将byte转成int的高于byte原本的8位的全部清除
		return b & 0xff;
	}

	/** 检测当前Message数据包的格式是否正确 */
	public boolean checkFormat() {
		// 如果这个Message的address是null 或者data是null
		// 或者data的长度小于SERVER_MESSAGE_MIN_LENGTH 就返回false
		if (address == null || data == null || data.length < Message.SERVER_MESSAGE_MIN_LENGTH) {
			return false;
		}
		// 获取Message的类型
		int cmd = getCmd();
		// 如果从data中得到的Message类型不是已经定义的几种类型，就返回false
		if (cmd != CMD_0x00 && cmd != CMD_0x10 && cmd != CMD_0x11 && cmd != CMD_0x20) {
			return false;
		}
		// 获取data中 Message消息数据的长度
		int dataLen = getContentLength();
		// 如果data的长度和从data中读取到得Message消息数据长度 + SERVER_MESSAGE_MIN_LENGTH（消息头）
		// 长度相等 就返回false
		// 所以 写入 data的Message消息数据长度 是纯消息数据的长度 不带消息包头的长度
		if (data.length != dataLen + SERVER_MESSAGE_MIN_LENGTH) {
			return false;
		}

		// 如果消息类型是 16 通用信息 并且Message消息数据长度 != 0 就返回false
		// 从这里看出 Message消息类型为 16 时 Message消息数据长度必须为 0
		if (cmd == CMD_0x10 && dataLen != 0) {
			return false;
		}
		// 如果消息类型是 17 分类消息 并且Message消息数据长度 != 8 就返回false
		// 从这里可以看出 Message消息类型为 17 时 Message消息数据长度必须为 8
		if (cmd == CMD_0x11 && dataLen != 8) {
			return false;
		}
		// 如果消息类型是 32 自定义消息 并且Message消息数据长度 < 1 就返回false
		// 从这里可以看出 Message消息类型为 32 时 Message消息数据长度必须 >= 1 才行
		// 就是说 消息类型为 自定义消息时 必须有消息数据
		if (cmd == CMD_0x20 && dataLen < 1) {// must has content
			return false;
		}
		// 如果以上各种检测判断都正确了。就表明这个数据包格式正确 返回true
		return true;
	}

	// 设置Message的data数据包
	public void setData(byte[] data) {
		this.data = data;
	}

	// 获取Message的data数据包
	public byte[] getData() {
		return this.data;
	}

	// 设置 IP套接字地址
	public void setSocketAddress(SocketAddress address) {
		this.address = address;
	}

	// 获取 IP套接字地址
	public SocketAddress getSocketAddress() {
		return this.address;
	}

	// 设置版本号 版本号必须 >= 1 && < 255
	public static void setVersion(int v) {
		if (v < 1 || v > 255) {
			return;
		}
		version = v;
	}

	// 获取版本号
	public static int getVersion() {
		return version;
	}

	/*
	 * public static void main(String[] args) { // 消息数据 String str = "TestData";
	 * System.out.println("msgData:" + str);
	 * 
	 * int strLen = str.getBytes().length; // 长度是 8
	 * System.out.println("msgDataLen:" + strLen);
	 * 
	 * // 存放消息长度 两位 // 这里 为什么吧数字放到第二位。首先由于是测试。内容长度小于byte的最大值128 // 就不去管超过127
	 * 我们的数字只有8 按2位来写。应该是 08 而不是 80 // 同时 我们读取的时候。也是从前向后读取的。所以需要吧数字写到第二个。 byte[]
	 * bb = new byte[2]; bb[0] = 0; bb[1] = (byte) strLen;
	 * 
	 * // 创建一个byte缓冲区 ByteBuffer buf = ByteBuffer.allocate(16); buf.put((byte)
	 * 0); buf.put((byte) 0); buf.put((byte) 17); // 信息类型 信息分类 buf.put(bb, 0,
	 * 2); // 两个byte Message消息数据长度 buf.put(str.getBytes()); // 消息内容
	 * 
	 * // 创建一个Message消息对象 Message msg = new Message(null, buf.array());
	 * 
	 * // 获取这个消息长度 int len = msg.getContentLength();
	 * System.out.println("getMessageContentLength:" + len);
	 * 
	 * // 获取消息类型 int cmd = msg.getCmd(); System.out.println("消息类型" + cmd); }
	 */
}
